Анализ рынка заведений общественного питания Москвы
- 1 Описание проекта
- 2 Обзор данных
- 3 Предобработка данных
- 4 Анализ рынка общепита
- 4.1 Заведенийя по категориям
- 4.2 Заведения по кол-ву посадочных мест в категориях
- 4.3 Соотношение сетевых/не сетевых заведений
- 4.4 Категории заведений в разрезе сетевых/не сетевых
- 4.5 ТОП-15 сетевых заведений Москвы
- 4.6 Типы заведений в разрезе районов Москвы
- 4.7 Распределение средних рейтингов по типам заведений
- 4.8 Средние рейтинги заведений по районам Москвы
- 4.9 Распределение заведений по районам Москвы
- 4.10 Топ-15 улиц Москвы по количеству заведений
- 4.11 Улицы, на которых находится только один объект общепита
- 4.12 Средний чек заведений по районам Москвы
- 4.13 Средний чек заведений по типам заведений
- 4.14 Анализ круглосуточной работы заведений по районам Москвы
- 4.15 Круглосуточная работа заведений по типам заведений
- 4.16 Корреляция между ключевыми параметрами
- 4.17 Вывод и рекомендации
- 5 Анализ кофеен
- 6 Вывод и рекомендации
- 7 Презентация
Описание проекта¶
Область исследования:
- датасет с заведениями общественного питания Москвы, составленный на основе данных сервисов Яндекс Карты и Яндекс Бизнес на лето 2022 года.
Задача
Исследовать рынок заведений общественного питания Москвы:
- найти интересные особенности, которые помогут в выборе формата и места для нового заведения;
- дополнительно сделать углубленное исследование рынка кофеен Москвы и дать рекомендации по открытию кофейни в формате «Central Perk» (сериал Friends);
- презентовать полученные результаты.
Структура данных:
- name — название заведения;
- address — адрес заведения;
- category — категория заведения, например «кафе», «пиццерия» или «кофейня»;
- hours — информация о днях и часах работы;
- lat — широта географической точки, в которой находится заведение;
- lng — долгота географической точки, в которой находится заведение;
- rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);
- price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;
- avg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:
- «Средний счёт: 1000–1500 ₽»;
- «Цена чашки капучино: 130–220 ₽»;
- «Цена бокала пива: 400–600 ₽» и так далее;
- middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»:
- Если в строке указан ценовой диапазон из двух значений, в столбец войдёт медиана этих двух значений.
- Если в строке указано одно число — цена без диапазона, то в столбец войдёт это число.
- Если значения нет или оно не начинается с подстроки «Средний счёт», то в столбец ничего не войдёт.
- middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца
- avg_bill, начинающихся с подстроки «Цена одной чашки капучино»:
- Если в строке указан ценовой диапазон из двух значений, в столбец войдёт медиана этих двух значений.
- Если в строке указано одно число — цена без диапазона, то в столбец войдёт это число.
- Если значения нет или оно не начинается с подстроки «Цена одной чашки капучино», то в столбец ничего не войдёт.
- chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым (для маленьких сетей могут встречаться ошибки):
- 0 — заведение не является сетевым
- 1 — заведение является сетевым
- district — административный район, в котором находится заведение, например Центральный административный округ;
- seats — количество посадочных мест.
Обзор данных¶
import pandas as pd
from fuzzywuzzy import fuzz
from fuzzywuzzy import process
from collections import defaultdict
from joblib import Parallel, delayed
import matplotlib.pyplot as plt
import plotly.express as px
import seaborn as sns
import folium
from folium import Map, Choropleth, GeoJson
from folium.plugins import MarkerCluster
from folium import GeoJsonTooltip
import json
from IPython.display import IFrame
try:
data = pd.read_csv('/datasets/moscow_places.csv', sep=',')
except:
data = pd.read_csv('/Users/mariiasergeeva/Desktop/moscow_places.csv',
sep=',')
data.sample(3)
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3082 | Испанские пончики Чуррос | булочная | Москва, Западный административный округ, район... | Западный административный округ | пн-чт 10:00–22:00; пт-вс 10:00–23:00 | 55.771874 | 37.435601 | 4.2 | NaN | NaN | NaN | NaN | 0 | NaN |
| 6206 | Щепка | кафе | Москва, Ленинский проспект, 60/2 | Юго-Западный административный округ | ежедневно, 09:00–21:00 | 55.696638 | 37.559151 | 3.8 | NaN | NaN | NaN | NaN | 1 | 34.0 |
| 7631 | Senza Titolo | пиццерия | Москва, Варшавское шоссе, 141к11 | Южный административный округ | ежедневно, 11:00–23:00 | 55.588092 | 37.601319 | 4.3 | средние | Средний счёт:500–1500 ₽ | 1000.0 | NaN | 0 | NaN |
data.info()
data.describe()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 3315 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 919.5+ KB
| lat | lng | rating | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|
| count | 8406.000000 | 8406.000000 | 8406.000000 | 3149.000000 | 535.000000 | 8406.000000 | 4795.000000 |
| mean | 55.750109 | 37.608570 | 4.229895 | 958.053668 | 174.721495 | 0.381275 | 108.421689 |
| std | 0.069658 | 0.098597 | 0.470348 | 1009.732845 | 88.951103 | 0.485729 | 122.833396 |
| min | 55.573942 | 37.355651 | 1.000000 | 0.000000 | 60.000000 | 0.000000 | 0.000000 |
| 25% | 55.705155 | 37.538583 | 4.100000 | 375.000000 | 124.500000 | 0.000000 | 40.000000 |
| 50% | 55.753425 | 37.605246 | 4.300000 | 750.000000 | 169.000000 | 0.000000 | 75.000000 |
| 75% | 55.795041 | 37.664792 | 4.400000 | 1250.000000 | 225.000000 | 1.000000 | 140.000000 |
| max | 55.928943 | 37.874466 | 5.000000 | 35000.000000 | 1568.000000 | 1.000000 | 1288.000000 |
data.hist(figsize=(15, 10), bins=30, color='#789DBC')
plt.show()
print('Количество заведений:', data.name.nunique())
Количество заведений: 5614
Вывод¶
Оценка "сверху":
- 8406 строк, типы данных: float64, int64, object;
- из 8406 строк только 5601 заведений имеют уникальное название. Это подтверждает график "chain", где видны те самые три с лишним тысячи сетевых заведений с неуникальными названиями;
- в части столбцов есть пропуски, доходящие до 95%. Это может быть связано с тем, что параметр столбца присущ не всем заведениям в датасете и поэтому значения остаются не заполненными.
Предобработка данных¶
Дубликаты¶
Оценим количество дубликатов:
print("Кол-во явных дубликатов:", data.duplicated().sum())
Кол-во явных дубликатов: 0
Явных дубликатов не найдено, но могут быть неявные.
Попробуем их определить с помощью алгоритма Левенштейна.
#снимаем ограничение на ширину столбцов для оценки дубликатов
pd.set_option('display.max_colwidth', None)
#функция для нахождения похожих названий
def find_similar_names(name, data, threshold=77):
similar_names = process.extract(name,
data['name'],
scorer=fuzz.token_sort_ratio)
return [(idx, score) for similar_name, score, idx in similar_names
if score >= threshold]
#функция для нахождения дубликатов с использованием алгоритма Левенштейна
def find_duplicates(data, threshold=77, n_jobs=-1):
#словарь для хранения схожести названий
similarity_dict = defaultdict(list)
#предварительная фильтрация данных по первой букве названия
data['name_initial'] = data['name'].str[0]
grouped_data = data.groupby('name_initial')
#параллельная обработка групп
results = Parallel(n_jobs=n_jobs)(delayed(process_group)(group, threshold)
for _, group in grouped_data)
#объединение результатов
for result in results:
for name, matches in result.items():
similarity_dict[name].extend(matches)
#создание фрейма для хранения дубликатов
duplicates = pd.DataFrame(columns=data.columns)
#проход по словарю и добавление дубликатов во фрейм
for name, matches in similarity_dict.items():
for idx, score in matches:
duplicates = pd.concat([duplicates, data.iloc[[idx]]])
return duplicates.drop_duplicates()
#функция для обработки группы данных
def process_group(group, threshold):
group_similarity_dict = defaultdict(list)
for i, row in group.iterrows():
name = row['name']
address = row['address']
#если название уже было обработано, пропускаем
if name in group_similarity_dict:
continue
#поиск похожих названий заведений
similar_names = find_similar_names(name, group, threshold)
#сохранение результатов в словарь
for idx, score in similar_names:
if group.at[idx, 'address'] == address and idx != i:
group_similarity_dict[name].append((idx, score))
return group_similarity_dict
#нахождение дубликатов
duplicates = find_duplicates(data)
#удаляем промежуточную колонку 'name_initial'
data = data.drop(columns=['name_initial'])
duplicates
/var/folders/yq/4kk1f65n4xx9ljs464yq0dvh0000gn/T/ipykernel_22006/18141113.py:34: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation. duplicates = pd.concat([duplicates, data.iloc[[idx]]])
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | name_initial | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3114 | Eshka | ресторан | Москва, Рублёвское шоссе, 42, корп. 1 | Западный административный округ | NaN | 55.750782 | 37.412500 | 3.4 | NaN | NaN | NaN | NaN | 0 | 164.0 | E |
| 3007 | Eshak | ресторан | Москва, Рублёвское шоссе, 42, корп. 1 | Западный административный округ | пн-чт 12:00–00:00; пт,сб 12:00–06:00; вс 12:00–00:00 | 55.751036 | 37.412361 | 4.5 | высокие | Средний счёт:2000–2500 ₽ | 2250.0 | NaN | 1 | 164.0 | E |
| 1511 | More Poke | ресторан | Москва, Волоколамское шоссе, 11, стр. 2 | Северный административный округ | пн-чт 09:00–18:00; пт,сб 09:00–21:00; вс 09:00–18:00 | 55.806307 | 37.497566 | 4.2 | NaN | NaN | NaN | NaN | 1 | 188.0 | M |
| 1430 | More poke | ресторан | Москва, Волоколамское шоссе, 11, стр. 2 | Северный административный округ | ежедневно, 09:00–21:00 | 55.806307 | 37.497566 | 4.2 | NaN | NaN | NaN | NaN | 0 | 188.0 | M |
| 5850 | VIP Wok and sushi | быстрое питание | Москва, Можайское шоссе, 45Б | Западный административный округ | ежедневно, 11:00–23:00 | 55.716420 | 37.406693 | 4.2 | NaN | NaN | NaN | NaN | 0 | 16.0 | V |
| 5764 | VIP Wok & sushi | быстрое питание | Москва, Можайское шоссе, 45Б | Западный административный округ | ежедневно, 11:00–23:00 | 55.716484 | 37.407236 | 4.2 | NaN | NaN | NaN | NaN | 0 | 16.0 | V |
| 600 | В парке вкуснее | кофейня | Москва, Северный административный округ, район Левобережный, территория парка Дружбы | Северный административный округ | ежедневно, 10:00–21:00 | 55.851985 | 37.478492 | 2.2 | NaN | NaN | NaN | NaN | 1 | NaN | В |
| 599 | В парке вкуснее! | кафе | Москва, Северный административный округ, район Левобережный, территория парка Дружбы | Северный административный округ | ежедневно, 10:00–21:00 | 55.854571 | 37.487254 | 2.2 | NaN | NaN | NaN | NaN | 0 | NaN | В |
| 6533 | Кувшинчик | ресторан | Москва, улица Академика Анохина, 58 | Западный административный округ | пн-чт 12:00–23:00; пт,сб 12:00–00:00; вс 12:00–23:00 | 55.650398 | 37.469449 | 4.6 | NaN | NaN | NaN | NaN | 0 | 280.0 | К |
| 6520 | Кувшин | ресторан | Москва, улица Академика Анохина, 58 | Западный административный округ | ежедневно, 12:00–23:00 | 55.650134 | 37.469377 | 4.6 | NaN | NaN | NaN | NaN | 0 | 280.0 | К |
| 2420 | Раковарня Клешни и хвосты | бар,паб | Москва, проспект Мира, 118 | Северо-Восточный административный округ | пн-чт 12:00–00:00; пт,сб 12:00–01:00; вс 12:00–00:00 | 55.810677 | 37.638379 | 4.4 | NaN | NaN | NaN | NaN | 1 | 150.0 | Р |
| 2211 | Раковарня Клешни и Хвосты | ресторан | Москва, проспект Мира, 118 | Северо-Восточный административный округ | ежедневно, 12:00–00:00 | 55.810553 | 37.638161 | 4.4 | NaN | NaN | NaN | NaN | 0 | 150.0 | Р |
| 1081 | Фуд-холл Рестомаркет | ресторан | Москва, Калужско-Рижская линия, метро ВДНХ | Северо-Восточный административный округ | ежедневно, 11:00–23:00 | 55.836031 | 37.624333 | 4.2 | NaN | NaN | NaN | NaN | 0 | NaN | Ф |
| 1037 | Фудкорт Рестомаркет | кафе | Москва, Калужско-Рижская линия, метро ВДНХ | Северо-Восточный административный округ | ежедневно, 11:00–23:00 | 55.831891 | 37.630745 | 4.2 | NaN | NaN | NaN | NaN | 0 | NaN | Ф |
| 3109 | Хлеб да выпечка | кафе | Москва, Ярцевская улица, 19 | Западный административный округ | NaN | 55.738449 | 37.410937 | 4.1 | NaN | NaN | NaN | NaN | 0 | 276.0 | Х |
| 3091 | Хлеб да Выпечка | булочная | Москва, Ярцевская улица, 19 | Западный административный округ | ежедневно, 09:00–22:00 | 55.738886 | 37.411648 | 4.1 | NaN | NaN | NaN | NaN | 1 | 276.0 | Х |
| 55 | Чебуреки и манты | кафе | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.880288 | 37.448645 | 4.3 | NaN | NaN | NaN | NaN | 0 | 148.0 | Ч |
| 20 | Чебуреки Манты | кафе | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.880287 | 37.448646 | 4.3 | NaN | NaN | NaN | NaN | 1 | 148.0 | Ч |
| 450 | Чайхана Халаль | ресторан | Москва, Смольная улица, 24Г, стр. 6 | Северный административный округ | ежедневно, круглосуточно | 55.860892 | 37.482538 | 4.3 | NaN | NaN | NaN | NaN | 1 | 53.0 | Ч |
| 416 | Чайхана Халал | ресторан | Москва, Смольная улица, 24Г, стр. 6 | Северный административный округ | ежедневно, круглосуточно | 55.860894 | 37.482553 | 4.3 | NaN | NaN | NaN | NaN | 0 | 53.0 | Ч |
Да, действительно, вся выдача подходит под категорию дубликатов, можем смело удалять.
data = data.drop(duplicates.index)
print("Количество удаленных дубликатов:", len(duplicates))
Количество удаленных дубликатов: 20
Хоть удалено строк не много, но данныестали качественнее. На будущих картах не будет лишних пинов, которые могут навести на ложные выводы.
Пропуски¶
Оценим количество пропусков:
#число пропусков
missing_values = data.isna().sum()
#процент пропусков
missing_percentage = (missing_values / len(data)) * 100
#собираем в итоговую таблицу
missing_data_table = pd.DataFrame({
'Столбец':
missing_values.index,
'Кол-во пропусков':
missing_values.values,
'% пропусков':
missing_percentage.values.round(0)
})
missing_data_table
| Столбец | Кол-во пропусков | % пропусков | |
|---|---|---|---|
| 0 | name | 0 | 0.0 |
| 1 | category | 0 | 0.0 |
| 2 | address | 0 | 0.0 |
| 3 | district | 0 | 0.0 |
| 4 | hours | 534 | 6.0 |
| 5 | lat | 0 | 0.0 |
| 6 | lng | 0 | 0.0 |
| 7 | rating | 0 | 0.0 |
| 8 | price | 5072 | 60.0 |
| 9 | avg_bill | 4571 | 55.0 |
| 10 | middle_avg_bill | 5238 | 62.0 |
| 11 | middle_coffee_cup | 7851 | 94.0 |
| 12 | chain | 0 | 0.0 |
| 13 | seats | 3607 | 43.0 |
Самый большой массив данных отсутствует в сегменте цен, пересекающихся колонках:
- price
- avg_bill
- middle_avg_bill
- middle_coffee_cup
Предположим, что пропуски в одной колонке мы сможем перекрыть за счет сопоставления значений из других. Для этого необходимо понять, насколько пропуски пересекаются во всех колонках одновременно (= не сможем восстановить значение в колонке за счет данных из других).
#определим столбцы, которые будем анализировать
columns_with_na = ['price', 'avg_bill', 'middle_avg_bill', 'middle_coffee_cup']
#выявляем строки, где пропуски есть во всех столбцах одновременно
rows_with_all_na = data[columns_with_na].isna().all(axis=1)
#считаем такие строки
num_rows_with_all_na = rows_with_all_na.sum()
print(
f"Количество строк с пропусками во всех указанных столбцах одновременно: {num_rows_with_all_na}"
)
Количество строк с пропусками во всех указанных столбцах одновременно: 4329
Выходит, что практически 80% строк перекрестно восстановить не сможем.
Кажется, что на текущем этапе пропуски заполнять не имеет смысла.
Создание столбцов¶
столбец с названиями улиц¶
#вернем ограничение ширины столбцов для компактности
pd.reset_option('display.max_colwidth')
def st(row):
add = row['address'].split(', ')
st = add[1]
return st
data['street'] = data.apply(st, axis=1)
data.sample(3)
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | street | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 4051 | Mad Season | бар,паб | Москва, Зубовский бульвар, 15, стр. 1 | Центральный административный округ | пн-чт 14:00–01:00; пт,сб 14:00–02:00; вс 14:00... | 55.735816 | 37.591423 | 4.6 | средние | Цена бокала пива:240–350 ₽ | NaN | NaN | 0 | 320.0 | Зубовский бульвар |
| 304 | Forest Lounge | бар,паб | Москва, Ленинградское шоссе, 51, стр. 4 | Северный административный округ | пн-чт 12:00–00:00; пт-вс 12:00–01:00 | 55.849573 | 37.469999 | 4.3 | NaN | NaN | NaN | NaN | 0 | 230.0 | Ленинградское шоссе |
| 5461 | Суши Соро | пиццерия | Москва, Новокосинская улица, 47 | Восточный административный округ | ежедневно, 10:00–22:00 | 55.743987 | 37.874466 | 4.9 | NaN | NaN | NaN | NaN | 0 | 19.0 | Новокосинская улица |
столбец с графиком работы¶
data['is_24_7'] = data[
data['hours'].apply(lambda x: 'ежедневно, круглосуточно' in str(x)
)]['hours'] == 'ежедневно, круглосуточно'
data['is_24_7'] = data['is_24_7'].fillna(False)
data.sample(3)
/var/folders/yq/4kk1f65n4xx9ljs464yq0dvh0000gn/T/ipykernel_22006/4015954668.py:4: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
data['is_24_7'] = data['is_24_7'].fillna(False)
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | street | is_24_7 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 7770 | Суши лавка | кафе | Москва, улица Академика Янгеля, 6, корп. 1 | Южный административный округ | NaN | 55.595902 | 37.592132 | 4.5 | NaN | NaN | NaN | NaN | 0 | 20.0 | улица Академика Янгеля | False |
| 863 | Викрама | столовая | Москва, Большая Академическая улица, 44, корп. 2 | Северный административный округ | пн-пт 09:00–17:00 | 55.839315 | 37.549892 | 4.0 | средние | Средний счёт:100–700 ₽ | 400.0 | NaN | 0 | 40.0 | Большая Академическая улица | False |
| 7708 | Донер бистро | быстрое питание | Москва, Кировоградская улица, вл15А | Южный административный округ | ежедневно, круглосуточно | 55.609970 | 37.603600 | 4.3 | NaN | NaN | NaN | NaN | 0 | NaN | Кировоградская улица | True |
Вывод¶
При анализе и предобработке данных:
- явных дубликатов не выявлено;
- неявных дубликатов выявлено и удалено 20;
- большой массив данных отсутствует в сегменте цен (колонки: price, avg_bill, middle_avg_bill, middle_coffee_cup). Принято решение о том, чтобы их не заполнять на текущем этапе анализа;
- создан столбец с отдельным названием улицы из общего адреса;
- создан столбец с маркировкой заведений с графиком работы - 24/7.
Анализ рынка общепита¶
Заведенийя по категориям¶
Оценим распределение заведений по категориям:
category = data.groupby('category')['name'].count().sort_values(
ascending=False).reset_index()
category = category.rename(columns={'name': 'count'})
category
| category | count | |
|---|---|---|
| 0 | кафе | 2373 |
| 1 | ресторан | 2033 |
| 2 | кофейня | 1412 |
| 3 | бар,паб | 764 |
| 4 | пиццерия | 633 |
| 5 | быстрое питание | 601 |
| 6 | столовая | 315 |
| 7 | булочная | 255 |
Отобразим распределение на графике:
#строим график
fig = px.bar(category,
x='category',
y='count',
title='Распределение категорий заведений',
labels={
'category': 'Категория',
'count': 'Кол-во заведений'
},
text='count')
#настраиваем параметры
fig.update_traces(texttemplate='%{text}',
textposition='outside',
marker_color='#789DBC')
# сохраняем график
fig.write_image("distribution_of_categories.png", format="png", scale=2)
fig.show()
Промежуточный вывод
Топ-3 типов заведений:
- Кафе
- Ресторан
- Кофейня
Заведения по кол-ву посадочных мест в категориях¶
Оценим средние и медианные значения посадочных мест для категорий:
#рассчитываем медиану
median_seats = data.groupby('category')['seats'].median().sort_values(
ascending=False).reset_index()
median_seats = median_seats.rename(columns={'seats': 'median'})
#рассчитываем среднее
mean_seats = data.groupby('category')['seats'].mean().sort_values(
ascending=False).reset_index()
mean_seats = mean_seats.rename(columns={'seats': 'mean'})
#округляем значения
mean_seats['mean'] = mean_seats['mean'].round()
#собираем таблицу
summary_table = pd.merge(median_seats, mean_seats, on='category')
summary_table
| category | median | mean | |
|---|---|---|---|
| 0 | ресторан | 86.0 | 122.0 |
| 1 | бар,паб | 82.0 | 124.0 |
| 2 | кофейня | 80.0 | 111.0 |
| 3 | столовая | 75.5 | 100.0 |
| 4 | быстрое питание | 69.0 | 99.0 |
| 5 | кафе | 60.0 | 97.0 |
| 6 | пиццерия | 55.0 | 94.0 |
| 7 | булочная | 50.0 | 88.0 |
Отобразим на графике средние значения посадочных мест:
#строим график
fig = px.bar(
summary_table,
x='category',
y='mean',
title='Среднее количество посадочных мест по категориям заведений',
labels={
'category': 'Категория',
'mean': 'Среднее кол-во мест'
},
text='mean')
#настраиваем параметры
fig.update_traces(texttemplate='%{text}',
textposition='outside',
marker_color='#789DBC')
fig.show()
Разброс между средним и медианой кажется достаточно большим. Возможно есть выбросы в значениях.
Оценим данные на графиках:
#строим график
fig = px.box(data,
x='category',
y='seats',
title='Количество посадочных мест по категориям заведений',
labels={
'category': 'Категория',
'seats': 'Кол-во посадочных мест'
})
#настраиваем цвет и описание
fig.update_traces(marker_color='#789DBC', line_color='#789DBC')
fig.update_layout(xaxis_title='Категория',
yaxis_title='Кол-во посадочных мест')
fig.show()
Да, действительно, есть явные выбросы и повторяющиеся значения от категории в категорию.
Отсечем все значения после 800. Нули отсекать не будем, т.к. есть заведения, работающие только на вынос.
Снова взглянем на графики:
#фильтруем данные
filtered_data = data.query('seats <= 800')
#строим график
fig = px.box(filtered_data,
x='category',
y='seats',
title='Количество посадочных мест по категориям заведений',
labels={
'category': 'Категория',
'seats': 'Кол-во посадочных мест'
})
#настраиваем цвет и описание
fig.update_traces(marker_color='#789DBC', line_color='#789DBC')
fig.update_layout(xaxis_title='Категория',
yaxis_title='Кол-во посадочных мест')
fig.show()
Промежуточный вывод
Интересно отметить, что не смотря на тип заведения, все они колеблятся по посадке между 25 и 150 местами.
Наиболее разнообразными по посадке являются кафе.
Предполагаем, что этот тип может содержать в себе разные форматы, от столовой до ресторана.
Соотношение сетевых/не сетевых заведений¶
#соберем таблицу с соотношением сетевых и не сетевых заведений
chain_counts = data['chain'].value_counts().reset_index()
chain_counts.columns = ['chain', 'count']
chain_counts['chain'] = chain_counts['chain'].map({
0: 'Не сетевое',
1: 'Сетевое'
})
display(chain_counts)
#построим круговую диаграмму
color_discrete_map = {'Не сетевое': '#789DBC', 'Сетевое': '#FFDDAE'}
# Построим круговую диаграмму
fig = px.pie(chain_counts,
names='chain',
values='count',
title='Соотношение сетевых и не сетевых заведений',
color='chain',
color_discrete_map=color_discrete_map)
fig.show()
| chain | count | |
|---|---|---|
| 0 | Не сетевое | 5188 |
| 1 | Сетевое | 3198 |
Промежуточный вывод
Не сетевых заведений больше почти в полтора раза.
Категории заведений в разрезе сетевых/не сетевых¶
#сгруппируем данные по категориям и типам
grouped_data = data.groupby(['category',
'chain']).size().reset_index(name='count')
grouped_data['chain'] = grouped_data['chain'].map({
0: 'Не сетевое',
1: 'Сетевое'
})
#группируем данные для сортировки столбцов
total_counts = grouped_data.groupby('category')['count'].sum().reset_index()
total_counts = total_counts.rename(columns={'count': 'total_count'})
sorted_data = pd.merge(grouped_data, total_counts, on='category')
sorted_data = sorted_data.sort_values(by='total_count', ascending=False)
#строим диаграмму
fig = px.bar(sorted_data,
x='category',
y='count',
color='chain',
title='Соотношение сетевых и не сетевых заведений по категориям',
labels={
'category': 'Категория',
'count': 'Кол-во заведений',
'chain': 'Тип заведения'
},
color_discrete_map={
'Сетевое': '#FFDDAE',
'Не сетевое': '#789DBC'
},
category_orders={'category': sorted_data['category'].unique()})
fig.show()
К сетевым заведениям чаще всего относятся кофейни, пиццерии и булочные.
Возможно, это связано с низким порогом входа для этих типов заведений, а так же развитая система франшиз.
Отобразим соотношение долей сетевых заведений по категориям:
Промежуточный вывод
Если внутри своего типа заведения, среди пиццерий и булочных много сетевых, то в общем массе общепита Москвы, больше сетевых кафе, кофеинь и ресторанов.
chain = data.groupby('category').agg({'chain':['count','sum']}).reset_index()
# считаем количество заведений по категориям и сколько из них сетевые
chain.columns = ['category','total_amnt','chain']
# переименовывем столбцы
chain['percent%'] = (chain['chain']/chain['total_amnt']*100).round(1)
# считаем долю сетевых от всех заведений в категориях, выводим на экран
chain = chain.sort_values(by='percent%',ascending=False)
display(chain.style.background_gradient(subset=['total_amnt','percent%'],cmap='Blues', axis=0))
order = list(chain['category'])
# список для сортировки графика
| category | total_amnt | chain | percent% | |
|---|---|---|---|---|
| 1 | булочная | 255 | 156 | 61.200000 |
| 5 | пиццерия | 633 | 330 | 52.100000 |
| 4 | кофейня | 1412 | 719 | 50.900000 |
| 2 | быстрое питание | 601 | 232 | 38.600000 |
| 6 | ресторан | 2033 | 727 | 35.800000 |
| 3 | кафе | 2373 | 778 | 32.800000 |
| 7 | столовая | 315 | 88 | 27.900000 |
| 0 | бар,паб | 764 | 168 | 22.000000 |
#строим график
fig = px.bar(chain,
x='percent%',
y='category',
orientation='h',
title='Доли сетевых заведений в каждом типе',
labels={
'percent%': 'Процент',
'category': 'Категория'
},
color='category',
color_discrete_map={
'ресторан': '#789DBC',
'кафе': '#ABBA7C',
'пиццерия': '#FFDDAE',
'бар,паб': '#C08497',
'быстрое питание': '#6B4226',
'булочная': '#8E7DBE',
'столовая': '#F4A259',
'кофейня': '#4D8B8B'
})
#настраиваем вид
fig.update_layout(yaxis={'categoryorder': 'total ascending'})
fig.show()
ТОП-15 сетевых заведений Москвы¶
df_chain = data[data['chain'] == 1]
top_15_chain = df_chain.groupby('name').agg({
'category': pd.Series.mode,
'district': 'count'
})
top_15_chain = top_15_chain.rename(columns={'district': 'count'})
top_15_chain = top_15_chain.sort_values('count',
ascending=False).reset_index().head(15)
top_15_chain
| name | category | count | |
|---|---|---|---|
| 0 | Шоколадница | кофейня | 120 |
| 1 | Домино'с Пицца | пиццерия | 76 |
| 2 | Додо Пицца | пиццерия | 74 |
| 3 | One Price Coffee | кофейня | 71 |
| 4 | Яндекс Лавка | ресторан | 69 |
| 5 | Cofix | кофейня | 65 |
| 6 | Prime | ресторан | 50 |
| 7 | Хинкальная | кафе | 44 |
| 8 | КОФЕПОРТ | кофейня | 42 |
| 9 | Кулинарная лавка братьев Караваевых | кафе | 39 |
| 10 | Теремок | ресторан | 38 |
| 11 | Чайхана | кафе | 37 |
| 12 | Буханка | булочная | 32 |
| 13 | CofeFest | кофейня | 32 |
| 14 | Му-Му | кафе | 27 |
#стоим график
fig = px.bar(top_15_chain,
x='count',
y='name',
orientation='h',
title='Топ-15 популярных сетей в Москве',
labels={
'count': 'Кол-во заведений',
'name': 'Сеть'
},
color_discrete_sequence=['#789DBC'])
#настраиваем вид
fig.update_layout(yaxis={'categoryorder': 'total ascending'})
fig.show()
Оценим, по каким категориям распределяюется этот топ:
#строим круговую диаграмму
fig_pie = px.pie(top_15_chain,
values='count',
names='category',
title='Доли типов сетевых заведений',
color='category',
color_discrete_map={
'ресторан': '#789DBC',
'кафе': '#ABBA7C',
'пиццерия': '#FFDDAE',
'бар,паб': '#C08497',
'быстрое питание': '#6B4226',
'булочная': '#8E7DBE',
'столовая': '#F4A259',
'кофейня': '#4D8B8B'
})
fig_pie.show()
Промежуточный вывод
Самыми популярными сетевыми заведениями являются кофейни, пиццерии, кафе, рестораны и булочные.
Их объединяет несколько параметров:
- возраст: Шоколадница одна из самых старых сетей кофеен в Москве и не только;
- франшизность: Додо, Cofix, Домино'с;
- доступная цена: все перечисленные выше заведения + братья Караваевы, Теремок.
Типы заведений в разрезе районов Москвы¶
Оценим, какие вообще округа есть в датасете:
data['district'].unique()
array(['Северный административный округ',
'Северо-Восточный административный округ',
'Северо-Западный административный округ',
'Западный административный округ',
'Центральный административный округ',
'Восточный административный округ',
'Юго-Восточный административный округ',
'Южный административный округ',
'Юго-Западный административный округ'], dtype=object)
#сгруппируем данные по районам и типам заведений и посчитаем количество заведений
grouped_data = data.groupby(['district',
'category']).size().reset_index(name='count')
#убираем 'административный округ' из названий районов
grouped_data['district'] = grouped_data['district'].str.replace(
' административный округ', '', regex=False)
#группируем данные для сортировки столбцов
total_counts = grouped_data.groupby('district')['count'].sum().reset_index()
total_counts = total_counts.rename(columns={'count': 'total_count'})
#мержим данные, чтобы добавить общую сумму заведений для каждого района
sorted_data = pd.merge(grouped_data, total_counts, on='district')
#сортируем по столбцам и внутри них
sorted_data = sorted_data.sort_values(by=['total_count', 'count'],
ascending=[False, False])
#выставляем палитру для диаграммы
color_discrete_map = {
'ресторан': '#789DBC',
'кафе': '#ABBA7C',
'пиццерия': '#FFDDAE',
'бар,паб': '#C08497',
'быстрое питание': '#6B4226',
'булочная': '#8E7DBE',
'столовая': '#F4A259',
'кофейня': '#4D8B8B'
}
#строим диаграмму
fig = px.bar(sorted_data,
x='count',
y='district',
color='category',
title='Количество заведений по районам и типам',
labels={
'district': 'Округ',
'count': 'Кол-во заведений',
'category': 'Тип заведения'
},
color_discrete_map=color_discrete_map,
category_orders={'district': sorted_data['district'].unique()})
fig.show()
Для более детальной оценки, соберем сводную таблицу для этого же разреза:
#создаем сводную таблицу
pivot_table = grouped_data.pivot_table(index='category',
columns='district',
values='count',
fill_value=0)
#сбросим индексы и удалим ненужную первую колонку
pivot_table.reset_index(inplace=True)
pivot_table.columns.name = None
pivot_table
| category | Восточный | Западный | Северный | Северо-Восточный | Северо-Западный | Центральный | Юго-Восточный | Юго-Западный | Южный | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | бар,паб | 53.0 | 50.0 | 68.0 | 62.0 | 23.0 | 364.0 | 38.0 | 38.0 | 68.0 |
| 1 | булочная | 25.0 | 36.0 | 39.0 | 28.0 | 12.0 | 50.0 | 13.0 | 27.0 | 25.0 |
| 2 | быстрое питание | 71.0 | 60.0 | 58.0 | 82.0 | 30.0 | 87.0 | 67.0 | 61.0 | 85.0 |
| 3 | кафе | 272.0 | 238.0 | 232.0 | 268.0 | 115.0 | 464.0 | 282.0 | 238.0 | 264.0 |
| 4 | кофейня | 105.0 | 150.0 | 192.0 | 159.0 | 62.0 | 428.0 | 89.0 | 96.0 | 131.0 |
| 5 | пиццерия | 72.0 | 71.0 | 77.0 | 68.0 | 40.0 | 113.0 | 55.0 | 64.0 | 73.0 |
| 6 | ресторан | 160.0 | 214.0 | 185.0 | 180.0 | 109.0 | 670.0 | 145.0 | 168.0 | 202.0 |
| 7 | столовая | 40.0 | 24.0 | 41.0 | 40.0 | 18.0 | 66.0 | 25.0 | 17.0 | 44.0 |
Промежуточный вывод
Больше всего заведений в Центральном округе, что свойственно тенденции в российских городах.
Наибольшую долю сохраняют популярные типы: ресторан, кафе, кофейня.
По остальным регионам похожее распределение среди популярных типов.
Самыми непопулярными остаются столовые и булочные.
Особенно их мало в Северо-Западном, Юго-Западном и Юго-Восточном округах.
Распределение средних рейтингов по типам заведений¶
Оценим средний и медианный рейтинг в абсолютном табличном виде:
#рассчитываем медиану
median_rating = data.groupby('category')['rating'].median().sort_values(
ascending=False).reset_index()
median_rating = median_rating.rename(columns={'rating': 'median'})
#рассчитываем среднее
mean_rating = data.groupby('category')['rating'].mean().sort_values(
ascending=False).reset_index()
mean_rating = mean_rating.rename(columns={'rating': 'mean'})
#округляем значения
mean_rating['mean'] = mean_rating['mean'].round(1)
#собираем таблицу
summary_table = pd.merge(median_rating, mean_rating, on='category')
summary_table
| category | median | mean | |
|---|---|---|---|
| 0 | бар,паб | 4.4 | 4.4 |
| 1 | булочная | 4.3 | 4.3 |
| 2 | кофейня | 4.3 | 4.3 |
| 3 | пиццерия | 4.3 | 4.3 |
| 4 | ресторан | 4.3 | 4.3 |
| 5 | столовая | 4.3 | 4.2 |
| 6 | быстрое питание | 4.2 | 4.0 |
| 7 | кафе | 4.2 | 4.1 |
Пока видим, что медианный и средний рейтинги радикально не различаются. Так же практически нет колебаний значений между типами заведений.
Оценим разброс рейтингов на "ящиках":
#строим график
fig = px.box(data,
x='category',
y='rating',
title='Рейтинг по категориям заведений',
labels={
'category': 'Категория',
'seats': 'Рейтинг'
})
#настраиваем цвет и описание
fig.update_traces(marker_color='#789DBC', line_color='#789DBC')
fig.update_layout(xaxis_title='Категория', yaxis_title='Рейтинг')
fig.show()
Промежуточный вывод
Практически по всем типам заведений средний и медианный рейтинг колеблется между 4.0 и 4.4 баллами.
Среди основных массивов отзывов:
- самые высокие рейтинги у баров и пабов;
- самые низкие и обширные по разнообразию оценок у быстрого питания и кафе;
- самые стабильные рейтинги у пиццерй и булочных.
Средние рейтинги заведений по районам Москвы¶
Рассчитаем средний рейтинг по районам:
mean_rating_by_district = data.groupby(
'district')['rating'].mean().reset_index()
mean_rating_by_district['rating'] = mean_rating_by_district['rating'].round(1)
mean_rating_by_district
| district | rating | |
|---|---|---|
| 0 | Восточный административный округ | 4.2 |
| 1 | Западный административный округ | 4.2 |
| 2 | Северный административный округ | 4.2 |
| 3 | Северо-Восточный административный округ | 4.1 |
| 4 | Северо-Западный административный округ | 4.2 |
| 5 | Центральный административный округ | 4.4 |
| 6 | Юго-Восточный административный округ | 4.1 |
| 7 | Юго-Западный административный округ | 4.2 |
| 8 | Южный административный округ | 4.2 |
Загрузим геоданные округов:
try:
geojson_path = '/Users/mariiasergeeva/Desktop/admin_level_geomap.geojson'
with open(geojson_path, 'r') as f:
geo_json = json.load(f)
except:
geojson_path = '/datasets/admin_level_geomap.geojson'
with open(geojson_path, 'r') as f:
geo_json = json.load(f)
print(json.dumps(geo_json, indent=2, ensure_ascii=False, sort_keys=True))
{
"features": [
{
"geometry": {
"coordinates": [
[
[
[
37.8756653,
55.825342400000004
],
[
37.876001599999995,
55.8249027
],
[
37.8730967,
55.8237936
],
[
37.8689418,
55.8228697
],
[
37.869206999999996,
55.822363200000005
],
[
37.868322500000005,
55.822211100000004
],
[
37.867472,
55.823703
],
[
37.86851090000001,
55.82391880000001
],
[
37.87046039999999,
55.8242706
],
[
37.871727799999995,
55.8246505
],
[
37.8724412,
55.824851499999994
],
[
37.873601099999995,
55.8251836
],
[
37.874401799999994,
55.8252441
],
[
37.8748787,
55.825267399999994
],
[
37.8756653,
55.825342400000004
]
]
],
[
[
[
37.797732300000014,
55.99985
],
[
37.79532840000001,
55.9990248
],
[
37.793020500000004,
56.0011418
],
[
37.792486800000006,
56.0009504
],
[
37.79018290000001,
56.0001243
],
[
37.7887247,
56.0013886
],
[
37.791381200000004,
56.002270100000004
],
[
37.79097720000001,
56.0026548
],
[
37.79033860000001,
56.00251240000001
],
[
37.79001420000001,
56.002850200000005
],
[
37.791075500000005,
56.0032236
],
[
37.79213700000001,
56.003597
],
[
37.79290810000001,
56.0038699
],
[
37.793034600000006,
56.0037881
],
[
37.79353240000001,
56.0037672
],
[
37.79415630000001,
56.003185300000005
],
[
37.797732300000014,
55.99985
]
]
],
[
[
[
37.79895800000001,
56.009590100000004
],
[
37.79812700000001,
56.007685099999996
],
[
37.7986646,
56.0068026
],
[
37.79950730000001,
56.006109900000006
],
[
37.797426900000005,
56.0053804
],
[
37.7968617,
56.0046327
],
[
37.7936914,
56.005357800000006
],
[
37.793098500000006,
56.0055271
],
[
37.791304000000004,
56.0071472
],
[
37.791591100000005,
56.0074532
],
[
37.7932241,
56.0080698
],
[
37.79301430000001,
56.00823260000001
],
[
37.795332000000016,
56.00899870000001
],
[
37.7949344,
56.0092902
],
[
37.7979841,
56.00953210000001
],
[
37.79853170000001,
56.0096683
],
[
37.7988895,
56.0096066
],
[
37.79895800000001,
56.009590100000004
]
]
],
[
[
[
37.899785200000004,
55.8161364
],
[
37.8999346,
55.8151406
],
[
37.8994626,
55.8145828
],
[
37.897964699999996,
55.813788499999994
],
[
37.8972371,
55.813656599999995
],
[
37.896656300000004,
55.81365039999999
],
[
37.8960238,
55.813732
],
[
37.895435000000006,
55.81376039999999
],
[
37.8943798,
55.813931499999995
],
[
37.8930571,
55.81422599999999
],
[
37.892053700000005,
55.814827399999984
],
[
37.8914232,
55.815043599999996
],
[
37.8892963,
55.81517590000001
],
[
37.88888599999999,
55.8154787
],
[
37.8881524,
55.8156752
],
[
37.887902499999996,
55.815941699999996
],
[
37.886874199999994,
55.816531499999996
],
[
37.8861402,
55.8161663
],
[
37.8853211,
55.816661999999994
],
[
37.8841286,
55.816660799999994
],
[
37.88211890000001,
55.8154707
],
[
37.881639899999996,
55.815015
],
[
37.880831799999996,
55.814580799999995
],
[
37.88084249999999,
55.8143186
],
[
37.879824799999994,
55.81420459999999
],
[
37.879484299999994,
55.813472
],
[
37.87964159999999,
55.81326729999999
],
[
37.8789748,
55.812424899999996
],
[
37.878148100000004,
55.81142919999999
],
[
37.8773566,
55.81146719999999
],
[
37.8741278,
55.810693699999995
],
[
37.8735825,
55.8086963
],
[
37.870080699999995,
55.807760699999996
],
[
37.8711986,
55.8063292
],
[
37.8665868,
55.8042107
],
[
37.8657431,
55.804207999999996
],
[
37.863264699999995,
55.80742109999999
],
[
37.8628232,
55.8073119
],
[
37.861697299999996,
55.8086902
],
[
37.8602718,
55.80857580000001
],
[
37.860033800000004,
55.8084267
],
[
37.86068709999999,
55.807540599999996
],
[
37.85636949999999,
55.80604139999999
],
[
37.8561187,
55.806648499999994
],
[
37.8560108,
55.806588399999995
],
[
37.8521852,
55.8089604
],
[
37.85129959999999,
55.8088336
],
[
37.85069149999999,
55.8085496
],
[
37.85046939999999,
55.8086719
],
[
37.85023739999999,
55.808557
],
[
37.848871499999994,
55.8094232
],
[
37.84980899
Отобразим средний рейтинг по районам на карте:
#определяем координаты центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
#создаем карту Москвы
m = Map(location=[moscow_lat, moscow_lng],
zoom_start=10,
tiles='Cartodb Positron')
#cоздаем хороплет
Choropleth(
geo_data=geo_json,
data=mean_rating_by_district,
columns=['district', 'rating'],
key_on='feature.name',
fill_color='Blues',
fill_opacity=0.8,
line_opacity=0.2,
legend_name='Средний рейтинг заведений по районам',
).add_to(m)
#добавляем тултипы с названиями округов и обводки
GeoJson(geo_json,
name='districts',
style_function=lambda feature: {
'color': 'grey',
'weight': 0.8,
'fillOpacity': 0
},
tooltip=folium.GeoJsonTooltip(fields=['name'],
aliases=['Округ:'])).add_to(m)
#сохраняем карту в HTML файл и отображаем в блокноте с помощью IFrame
map_path = 'moscow_map.html'
m.save(map_path)
IFrame(map_path, width=1000, height=800)
Промежуточный вывод
Как видно из карты:
- наиболее высокий рейтинг имеют заведения в Центральном административном округе;
- наименее высокий у заведений Северо-Восточного и Юго-Восточного административных округов.
Распределение заведений по районам Москвы¶
Рассчитаем количество заведений по районам:
count_by_district = data['district'].value_counts().reset_index()
count_by_district.columns = ['district', 'count']
count_by_district
| district | count | |
|---|---|---|
| 0 | Центральный административный округ | 2242 |
| 1 | Северный административный округ | 892 |
| 2 | Южный административный округ | 892 |
| 3 | Северо-Восточный административный округ | 887 |
| 4 | Западный административный округ | 843 |
| 5 | Восточный административный округ | 798 |
| 6 | Юго-Восточный административный округ | 714 |
| 7 | Юго-Западный административный округ | 709 |
| 8 | Северо-Западный административный округ | 409 |
Отобразим на карте:
#координаты центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
#создаем карту Москвы
m = Map(location=[moscow_lat, moscow_lng],
zoom_start=10,
tiles='Cartodb Positron')
#создаем хороплет
Choropleth(
geo_data=geo_json,
data=count_by_district,
columns=['district', 'count'],
key_on='feature.name',
fill_color='Blues',
fill_opacity=0.8,
line_opacity=0.2,
legend_name='Количество заведений по районам',
).add_to(m)
#добавляем границы округов
GeoJson(geo_json,
name='districts',
style_function=lambda feature: {
'color': 'grey',
'weight': 0.8,
'fillOpacity': 0
},
tooltip=folium.GeoJsonTooltip(fields=['name'],
aliases=['Округ:'])).add_to(m)
#создаем кластеры
marker_cluster = MarkerCluster().add_to(m)
#добавляем маркеры на карту
for idx, row in data.iterrows():
folium.Marker(
location=[row['lat'], row['lng']],
popup=folium.Popup(
f"{row['name']}<br>Категория: {row['category']}<br>Рейтинг: {row['rating']}",
max_width=300),
tooltip=row['name']).add_to(marker_cluster)
#сохраняем карту в HTML файл и отображаем в блокноте с помощью IFrame
map_path = 'moscow_map_clusters.html'
m.save(map_path)
IFrame(map_path, width=1000, height=600)
Промежуточный вывод
Центральный район лидируйет по количеству заведений с большим отрывом от остальных районов города.
Меньше всего заведений в Северо-Западном административном округе.
Топ-15 улиц Москвы по количеству заведений¶
#группируем данные по улицам и категориям заведений, посчитаем количество заведений на каждой улице
grouped_data = data.groupby(['street',
'category']).size().reset_index(name='count')
#группируем данные для сортировки столбцов
total_counts = grouped_data.groupby('street')['count'].sum().reset_index()
total_counts = total_counts.rename(columns={'count': 'total_count'})
#сортируем и выбираем топ-15
top_15_streets = total_counts.sort_values(by='total_count',
ascending=False).head(15)
#фильтруем данные только по улицам
filtered_data = grouped_data[grouped_data['street'].isin(
top_15_streets['street'])]
#сортируем данные внутри каждого столбца по количеству заведений
filtered_data = filtered_data.sort_values(by=['street', 'count'],
ascending=[True, False])
#выставляем палитру для диаграммы
color_discrete_map = {
'ресторан': '#789DBC',
'кафе': '#ABBA7C',
'пиццерия': '#FFDDAE',
'бар,паб': '#C08497',
'быстрое питание': '#6B4226',
'булочная': '#8E7DBE',
'столовая': '#F4A259',
'кофейня': '#4D8B8B'
}
#строим диаграмму
fig = px.bar(filtered_data,
x='count',
y='street',
color='category',
title='Топ-15 улиц по количеству заведений и их категориям',
labels={
'street': 'Улица',
'count': 'Кол-во заведений',
'category': 'Тип заведения'
},
color_discrete_map=color_discrete_map,
category_orders={'street': top_15_streets['street'].unique()})
fig.show()
Для кросс-чека, соберем сводную таблицу для этого же разреза:
#создаем сводную таблицу
pivot_table = filtered_data.pivot_table(index='category',
columns='street',
values='count',
fill_value=0)
#сбросим индексы и удалим ненужную первую колонку
pivot_table.reset_index(inplace=True)
pivot_table.columns.name = None
pivot_table
| category | Варшавское шоссе | Дмитровское шоссе | Каширское шоссе | Кутузовский проспект | Ленинградский проспект | Ленинградское шоссе | Ленинский проспект | Люблинская улица | МКАД | Профсоюзная улица | Пятницкая улица | проспект Вернадского | проспект Мира | улица Вавилова | улица Миклухо-Маклая | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | бар,паб | 6.0 | 6.0 | 2.0 | 2.0 | 15.0 | 5.0 | 10.0 | 5.0 | 1.0 | 6.0 | 9.0 | 7.0 | 11.0 | 2.0 | 3.0 |
| 1 | булочная | 0.0 | 2.0 | 0.0 | 1.0 | 4.0 | 2.0 | 3.0 | 0.0 | 0.0 | 4.0 | 3.0 | 1.0 | 4.0 | 2.0 | 0.0 |
| 2 | быстрое питание | 7.0 | 10.0 | 10.0 | 2.0 | 2.0 | 5.0 | 2.0 | 5.0 | 9.0 | 15.0 | 2.0 | 12.0 | 21.0 | 11.0 | 4.0 |
| 3 | кафе | 18.0 | 23.0 | 20.0 | 14.0 | 12.0 | 13.0 | 26.0 | 26.0 | 45.0 | 35.0 | 7.0 | 25.0 | 53.0 | 15.0 | 21.0 |
| 4 | кофейня | 14.0 | 11.0 | 16.0 | 13.0 | 25.0 | 13.0 | 23.0 | 11.0 | 4.0 | 18.0 | 6.0 | 16.0 | 36.0 | 10.0 | 4.0 |
| 5 | пиццерия | 4.0 | 8.0 | 5.0 | 3.0 | 9.0 | 3.0 | 5.0 | 1.0 | 0.0 | 15.0 | 3.0 | 12.0 | 11.0 | 3.0 | 2.0 |
| 6 | ресторан | 20.0 | 24.0 | 19.0 | 16.0 | 25.0 | 26.0 | 33.0 | 10.0 | 5.0 | 26.0 | 18.0 | 33.0 | 44.0 | 12.0 | 15.0 |
| 7 | столовая | 7.0 | 4.0 | 5.0 | 3.0 | 3.0 | 3.0 | 5.0 | 2.0 | 1.0 | 3.0 | 0.0 | 2.0 | 2.0 | 0.0 | 0.0 |
Промежуточный вывод
Проспект Мира является самой густонаселенной заведениями общепита. Сохраняется первенство типов заведений: ресторан, кафе, кофейня.
Улицы, на которых находится только один объект общепита¶
Сгруппируем данные по округам, типам заведений и выведем только те улицы, где есть одно заведение:
#группируем данные по улицам и фильтруем, где есть одно заведение
street_counts = data.groupby('street').size().reset_index(name='count')
streets_with_one_point = street_counts[street_counts['count'] == 1]
#объединяем с исходным фреймом
streets_with_one_point_district = pd.merge(
streets_with_one_point,
data[['street', 'district', 'category']].drop_duplicates(),
on='street',
how='left')
#группируем по округам
grouped_by_district_and_category = streets_with_one_point_district.groupby(
['district', 'category']).size().reset_index(name='number_of_streets')
#убираем 'административный округ' из названий районов
grouped_by_district_and_category[
'district'] = grouped_by_district_and_category['district'].str.replace(
' административный округ', '', regex=False)
#сортируем столбцы
sorted_data = grouped_by_district_and_category.sort_values(
by=['district', 'number_of_streets'], ascending=[False, False])
#сортируем столбцы
district_order = sorted_data.groupby(
'district')['number_of_streets'].sum().sort_values(ascending=False).index
#список уникальных категорий
category_order = sorted_data.groupby(
'category')['number_of_streets'].sum().sort_values(ascending=False).index
print(f"Количество улиц с одним заведением: {len(streets_with_one_point)}")
Количество улиц с одним заведением: 459
Построим график по данным:
#выставляем палитру для диаграммы
color_discrete_map = {
'ресторан': '#789DBC',
'кафе': '#ABBA7C',
'пиццерия': '#FFDDAE',
'бар,паб': '#C08497',
'быстрое питание': '#6B4226',
'булочная': '#8E7DBE',
'столовая': '#F4A259',
'кофейня': '#4D8B8B'
}
#строим диаграмму
fig = px.bar(sorted_data,
x='number_of_streets',
y='district',
color='category',
title='Число улиц Москвы с одним заведением по районам и типам',
labels={
'district': 'Округ',
'number_of_streets': 'Число улиц с одним заведением',
'category': 'Тип заведения'
},
orientation='h',
color_discrete_map=color_discrete_map,
category_orders={
'district': district_order,
'category': category_order
})
fig.show()
Промежуточный вывод
Всего в Москве 459 улиц с одним заведением, ЦАО лидирует и здесь.
Среди типов заведений лидирует кафе.
Средний чек заведений по районам Москвы¶
Рассчитываем медиану средних чеков по районам:
median_bill = data.groupby(
'district')['middle_avg_bill'].median().reset_index()
median_bill
| district | middle_avg_bill | |
|---|---|---|
| 0 | Восточный административный округ | 575.0 |
| 1 | Западный административный округ | 1000.0 |
| 2 | Северный административный округ | 650.0 |
| 3 | Северо-Восточный административный округ | 500.0 |
| 4 | Северо-Западный административный округ | 700.0 |
| 5 | Центральный административный округ | 1000.0 |
| 6 | Юго-Восточный административный округ | 450.0 |
| 7 | Юго-Западный административный округ | 600.0 |
| 8 | Южный административный округ | 500.0 |
Отобразим данные на карте:
#координаты для центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
#карта Москвы
m = folium.Map(location=[moscow_lat, moscow_lng],
zoom_start=10,
tiles='Cartodb Positron')
#строим хороплет
Choropleth(
geo_data=geo_json,
data=median_bill,
columns=['district', 'middle_avg_bill'],
key_on='feature.name',
fill_color='Blues',
fill_opacity=0.8,
line_opacity=0.2,
legend_name='Средний чек по районам (руб)',
).add_to(m)
#добавляем тултипы и границы для округов
GeoJson(geo_json,
name='districts',
style_function=lambda feature: {
'color': 'grey',
'weight': 0.8,
'fillOpacity': 0
},
tooltip=folium.GeoJsonTooltip(fields=['name'],
aliases=['Округ:'])).add_to(m)
#сохраняем карту в HTML файл и отображаем в блокноте с помощью IFrame
map_path = 'moscow_map.html'
m.save(map_path)
IFrame(map_path, width=1000, height=800)
Промежуточный вывод
Исходя из полученного графика, явно утверждать "чем дальше от центра, тем меньше средний чек" нельзя.
Зато явно видно, что:
- средний чек в Западном административном округе и Центральном одинаковы;
- чеки в Юго-Восточном, Северо-Восточном и Южном округах является самымы низким. Так же, они в 2 раза ниже, чем в Западном и Центральном округах.
Средний чек заведений по типам заведений¶
Выведем данные по средним чекам на типах заведений:
median_bill = data.groupby(
'category')['middle_avg_bill'].median().reset_index()
median_bill
| category | middle_avg_bill | |
|---|---|---|
| 0 | бар,паб | 1250.0 |
| 1 | булочная | 450.0 |
| 2 | быстрое питание | 375.0 |
| 3 | кафе | 550.0 |
| 4 | кофейня | 400.0 |
| 5 | пиццерия | 600.0 |
| 6 | ресторан | 1250.0 |
| 7 | столовая | 300.0 |
Построим график на основе данных:
#группируем данные по категориям и вычисление медианного значения среднего чека
median_bill = data.groupby(
'category')['middle_avg_bill'].median().reset_index()
#сортируем данные по убыванию медианного среднего чека
median_bill = median_bill.sort_values(by='middle_avg_bill', ascending=False)
#палитра для типов заведений
color_discrete_map = {
'ресторан': '#789DBC',
'кафе': '#ABBA7C',
'пиццерия': '#FFDDAE',
'бар,паб': '#C08497',
'быстрое питание': '#6B4226',
'булочная': '#8E7DBE',
'столовая': '#F4A259',
'кофейня': '#4D8B8B'
}
#строим диаграмму
fig = px.bar(
median_bill,
x='category',
y='middle_avg_bill',
color='category',
title='Медианные значения среднего чека по категориям заведений',
labels={
'middle_avg_bill': 'Медианный средний чек (руб.)',
'category': 'Тип заведения'
},
color_discrete_map=color_discrete_map,
orientation='v'
)
fig.show()
Промежуточный вывод
ТОП-3 типо заведений по среднему чеку:
- бар, паб
- ресторан
- пиццерия
Распределине рейтинга оказалось неочевидным: бары приносят схожий средний чек с ресторанами. А пиццерия приносит с одного клиента больше, чем остальные "малые формы" общепита.
Анализ круглосуточной работы заведений по районам Москвы¶
#фильтруем данные и оставляем круглосуточные
total_points_by_district = data.groupby('district').size().reset_index(
name='total_points')
round_the_clock_points = data[data['is_24_7'] == True].groupby(
'district').size().reset_index(name='round_the_clock')
#объединяем датафреймы и считаем процент
district_data = pd.merge(total_points_by_district,
round_the_clock_points,
on='district',
how='left')
district_data['round_the_clock_percent'] = (
district_data['round_the_clock'] / district_data['total_points']) * 100
#координаты центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
#создаем карту Москвы
m = folium.Map(location=[moscow_lat, moscow_lng],
zoom_start=10,
tiles='Cartodb Positron')
#строим хороплет
Choropleth(
geo_data=geo_json,
data=district_data,
columns=['district', 'round_the_clock_percent'],
key_on='feature.properties.name', # Путь к названию округа в geojson
fill_color='Blues',
fill_opacity=0.8,
line_opacity=0.2,
legend_name='Процент круглосуточных заведений по районам',
).add_to(m)
#добавляем тултипы для округов
GeoJson(geo_json,
name='districts',
style_function=lambda feature: {
'color': 'grey',
'weight': 0.8,
'fillOpacity': 0
},
tooltip=folium.GeoJsonTooltip(fields=['name'],
aliases=['Округ:'])).add_to(m)
#сохраняем карту в HTML файл и отображаем в блокноте с помощью IFrame
map_path = 'moscow_map_24_7.html'
m.save(map_path)
IFrame(map_path, width=1000, height=800)
Промежуточный вывод
Не смотря на плотность заведений в Центральном округе, круглосуточных среди них самый маленький процент.
Самый большой процент в Восточном и Юго-Восточном округах.
Круглосуточная работа заведений по типам заведений¶
#фильтруем данные
points = data[data['is_24_7'] == True]
#группируем данные
points_count = points.groupby('category').size().reset_index(name='count')
#сортируем по количеству
points_count_sorted = points_count.sort_values(by='count', ascending=False)
#строим диаграмму
fig = px.bar(points_count_sorted,
x='category',
y='count',
color='category',
title='Круглосуточные заведения по категориям',
labels={
'count': 'Количество круглосуточных заведений',
'category': 'Тип заведения'
},
color_discrete_map=color_discrete_map,
orientation='v')
fig.show()
points_count_sorted
| category | count | |
|---|---|---|
| 3 | кафе | 266 |
| 2 | быстрое питание | 150 |
| 6 | ресторан | 133 |
| 4 | кофейня | 59 |
| 0 | бар,паб | 52 |
| 5 | пиццерия | 31 |
| 1 | булочная | 24 |
| 7 | столовая | 12 |
В абсолютном выражении, по городу больше всего круглосуточных заведений из категорий:
- кафе
- быстрое питание
- ресторан
Попробуем соединить округа и типы заведений вместе:
#группируем данные по округам и категориям, считаем количество круглосуточных заведений
points_count = points.groupby(['district',
'category']).size().reset_index(name='count')
#убираем 'административный округ' из названий районов
points_count['district'] = points_count['district'].str.replace(
' административный округ', '', regex=False)
#сортируем
total_counts = points_count.groupby('district')['count'].sum().reset_index()
total_counts = total_counts.rename(columns={'count': 'total_count'})
#объединяем с исходными данными
sorted_data = pd.merge(points_count, total_counts, on='district')
#сортируем столбцы
sorted_data = sorted_data.sort_values(by=['total_count', 'count'],
ascending=[True, False])
#настроим палитру
color_discrete_map = {
'ресторан': '#789DBC',
'кафе': '#ABBA7C',
'пиццерия': '#FFDDAE',
'бар,паб': '#C08497',
'быстрое питание': '#6B4226',
'булочная': '#8E7DBE',
'столовая': '#F4A259',
'кофейня': '#4D8B8B'
}
#строим диаграмму
fig = px.bar(sorted_data,
x='count',
y='district',
color='category',
title='Круглосуточные заведения по округам',
labels={
'count': 'Количество круглосуточных заведений',
'district': 'Округ',
'category': 'Тип заведения'
},
color_discrete_map=color_discrete_map,
orientation='h')
fig.show()
Промежуточный вывод
Интересно отметить, что хоть в Центральном округе и меньше всего в процентах круглосуточных заведений, но в абсолютном выражении, всё ещё больше остальных округов.
Корреляция между ключевыми параметрами¶
Попробуем оценить корреляцию числовых параметров между собой:
# #объединяем исходный датафрейм с новыми столбцами
data_encoded = pd.concat([data,
#district_dummies
], axis=1)
#собираем список параметров для графика
cols = [
'middle_avg_bill', 'rating', 'is_24_7', 'chain', 'seats',
'middle_coffee_cup'
]
#рассчитываем матрицу
correlation_matrix = data_encoded[cols].corr()
#настраиваем и строим карту
plt.figure(figsize=(12, 8))
sns.heatmap(correlation_matrix,
annot=True,
cmap='mako',
center=0,
fmt='.2f',
linewidths=0.5,
vmin=-1,
vmax=1)
plt.title('Корреляции параметров заведений Москвы')
plt.show()
Промежуточный вывод
Стат значимой корреляции не обнаружено ни в одной паре парметров.
Вывод и рекомендации¶
В ходе анализа выявлено:¶
Популярность категорий
Самые популярные типы: кафе, рестораны, кофейни — лидируют по количеству заведений
Особенности: кафе отличаются универсальностью: подходят как для бюджетного, так и для премиум-сегмента
Вывод: эти форматы обладают высоким спросом и гибкостью для разной аудитории.
Размеры заведений
- Средняя вместимость: от 50 до 150 мест
- Лидеры по средней вместительности:
- Рестораны: 86 мест
- Бары/пабы: 82 места
- Меньше всего мест:
- Булочные: 50 мест
- Пиццерии: 55 мест
- Вывод: рестораны и бары подходят для крупных компаний, а булочные и пиццерии — для небольших уютных форматов.
Сетевые и не сетевые заведения
- Доля сетевых заведений: 38%
- Чаще всего сетевые форматы:кофейни, пиццерии, булочные — популярны благодаря франшизам
- Топовые бренды: «Шоколадница», «Додо Пицца», «One Price Coffee»
- Вывод: сетевые форматы привлекают за счёт узнаваемости и стандартизации.
Географическое распределение
- Лидеры по количеству заведений:
- ЦАО: 2242 заведения
- Следом идут Северный и Южный округа
- Средние рейтинги:
- ЦАО: 4.4 (выше среднего)
- Другие округа: 4.1–4.2
- Средние чеки:
- Самый доступный округ: Юго-Восточный (450 руб.)
- Дороже всего в ЦАО и на Западе (до 1000 руб.)
- Вывод: ЦАО привлекает за счёт высокого качества и премиальности, но бюджетные заведения находят спрос в менее дорогих округах.
Средние чеки по типам заведений
- Лидеры по чекам:
- Рестораны и бары/пабы: 1250 руб
- Средняя ценовая категория:
- Кафе и пиццерии: 550–600 руб
- Самые доступные форматы:
- Булочные: 450 руб
- Быстрое питание: 375 руб
- Вывод: выбор зависит от аудитории: премиум-сегмент выбирает рестораны, массовый — кафе и пиццерии.
Круглосуточная работа
- Лидеры по числу круглосуточных заведений: кафе и заведения быстрого питания
- Округа:
- Процент круглосуточных заведений выше в Восточном и Юго-Восточном округах
- Абсолютное число 24/7 заведений больше в ЦАО
- Вывод: Восток и Юго-Восток идеально подходят для круглосуточных форматов.
Корреляции
- Связей между параметрами не выявлено: рейтинги, средние чеки, круглосуточная работа и сетевой формат не имеют явной зависимости
- Вывод: успешный формат требует комплексного подхода и учёта нескольких факторов.
Рекомендации:¶
Тип заведения:
- Кафе или кофейня как универсальный формат
- Рестораны или бары — для премиум-сегмента
Расположение:
- ЦАО для премиум-форматов
- Восточные и Юго-Восточные округа для экономичных форматов или круглосуточных заведений
Формат:
- Рассмотреть возможность сетевого формата, особенно если речь идет о кофейнях или пиццериях. Обеспечит быструю масштабируемость
Чек:
- Средний чек 550–600 руб. — оптимальный для широкой аудитории
- Более детально:
- для кафе/кофейни - 500–600 рублей, чтобы охватить средний сегмент
- если целевая аудитория премиум-сегмента, чек можно повысить до 1000–1250 рублей.
Время работы:
- Включение круглосуточного формата в концепцию кафе или быстрого питания увеличит привлекательность. Особенно для районов Восточного округа.
Масштабирование:
- Если проект успешен, стоит начать с одного заведения, а затем масштабироваться в Восточных и Северо-Западных округах, где меньше конкуренции, но сохраняется стабильный спрос.
Анализ кофеен¶
Кофейни по районам Москвы¶
Посчитаем кол-во кофеен в датасете:
#фильтруем данные только по кофейням
coffee_shops = data[data['category'].str.contains('кофейня',
case=False,
na=False)]
#считаем кол-во
total_coffee_shops = coffee_shops.shape[0]
print(f"В датасете {total_coffee_shops} кофеен")
В датасете 1412 кофеен
Разобьем кофейни по округам:
coffee_shops_by_district = coffee_shops['district'].value_counts().reset_index(
)
coffee_shops_by_district.columns = ['district', 'count']
#убираем 'административный округ' из названий районов
coffee_shops_by_district['district'] = coffee_shops_by_district[
'district'].str.replace(' административный округ', '', regex=False)
print(f"Распределение кофеен по округам:")
coffee_shops_by_district
Распределение кофеен по округам:
| district | count | |
|---|---|---|
| 0 | Центральный | 428 |
| 1 | Северный | 192 |
| 2 | Северо-Восточный | 159 |
| 3 | Западный | 150 |
| 4 | Южный | 131 |
| 5 | Восточный | 105 |
| 6 | Юго-Западный | 96 |
| 7 | Юго-Восточный | 89 |
| 8 | Северо-Западный | 62 |
#строим график
fig = px.bar(coffee_shops_by_district,
x='district',
y='count',
title='Распределение кофеен по округам',
labels={
'district': 'Округ',
'count': 'Кол-во кофеен'
},
text='count')
#настраиваем параметры
fig.update_traces(texttemplate='%{text}',
textposition='outside',
marker_color='#789DBC')
fig.show()
Оценим особенности кофеен в округах по рейтингу и среднему чеку на кофе:
district_analysis = coffee_shops.groupby('district').agg(
count=('name', 'count'),
avg_rating=('rating', 'mean'),
avg_coffee_price=('middle_coffee_cup', 'mean')).round({
'avg_rating': 1,
'avg_coffee_price': 0
}).sort_values(by='count', ascending=False).reset_index()
district_analysis
| district | count | avg_rating | avg_coffee_price | |
|---|---|---|---|---|
| 0 | Центральный административный округ | 428 | 4.3 | 188.0 |
| 1 | Северный административный округ | 192 | 4.3 | 166.0 |
| 2 | Северо-Восточный административный округ | 159 | 4.2 | 165.0 |
| 3 | Западный административный округ | 150 | 4.2 | 190.0 |
| 4 | Южный административный округ | 131 | 4.2 | 158.0 |
| 5 | Восточный административный округ | 105 | 4.3 | 174.0 |
| 6 | Юго-Западный административный округ | 96 | 4.3 | 184.0 |
| 7 | Юго-Восточный административный округ | 89 | 4.2 | 151.0 |
| 8 | Северо-Западный административный округ | 62 | 4.3 | 166.0 |
Промежуточный вывод
- ЦАО остается на первом месте и по количеству кофеен;
- средний рейтинг заведений по округам варьируется от 4.2 до 4.3;
- более высокие средние чеки наблюдаются в Западном и Центральном округах, в то время как более доступные — в Юго-Восточном округе, что может указывать на разные целевые аудитории.
Круглосуточный режим работы кофеен¶
Выведем в таблице и круговой диаграмме распределение по графику работы:
#группируем по признаку круглосуточности
coffee_24_7_counts = coffee_shops['is_24_7'].value_counts().reset_index()
coffee_24_7_counts.columns = ['is_24_7', 'count']
coffee_24_7_counts['is_24_7'] = coffee_24_7_counts['is_24_7'].map({
False:
'Не круглосуточное',
True:
'Круглосуточное'
})
display(coffee_24_7_counts)
#строим диаграмму
color_discrete_map = {
'Круглосуточное': '#FFDDAE',
'Не круглосуточное': '#789DBC'
}
fig = px.pie(coffee_24_7_counts,
names='is_24_7',
values='count',
title='Соотношение круглосуточных и не круглосуточных кофеен',
color='is_24_7',
color_discrete_map=color_discrete_map,
labels={'is_24_7': 'График работы'})
fig.show()
| is_24_7 | count | |
|---|---|---|
| 0 | Не круглосуточное | 1353 |
| 1 | Круглосуточное | 59 |
Оценим распределение по округам:
#группируем данные по категориям и типам
grouped_data = coffee_shops.groupby(['district', 'is_24_7'
]).size().reset_index(name='count')
#переименовываем значения is_24_7
grouped_data['is_24_7'] = grouped_data['is_24_7'].map({
True: 'Круглосуточно',
False: 'Не круглосуточно'
})
#убираем 'административный округ' из названий районов
grouped_data['district'] = grouped_data['district'].str.replace(
' административный округ', '', regex=False)
#группируем для сортировки столбцов
total_counts = grouped_data.groupby('district')['count'].sum().reset_index()
total_counts = total_counts.rename(columns={'count': 'total_count'})
sorted_data = pd.merge(grouped_data, total_counts, on='district')
sorted_data = sorted_data.sort_values(by='total_count', ascending=False)
#строим диаграмму
fig = px.bar(
sorted_data,
x='district',
y='count',
color='is_24_7',
title='Распределение круглосуточных и не круглосуточных кофеен по районам',
labels={
'district': 'Район',
'count': 'Кол-во кофеен',
'is_24_7': 'График работы'
},
color_discrete_map={
'Круглосуточно': '#FFDDAE',
'Не круглосуточно': '#789DBC'
},
category_orders={'district': sorted_data['district'].unique()})
fig.show()
sorted_data
| district | is_24_7 | count | total_count | |
|---|---|---|---|---|
| 10 | Центральный | Не круглосуточно | 402 | 428 |
| 11 | Центральный | Круглосуточно | 26 | 428 |
| 4 | Северный | Не круглосуточно | 187 | 192 |
| 5 | Северный | Круглосуточно | 5 | 192 |
| 6 | Северо-Восточный | Не круглосуточно | 156 | 159 |
| 7 | Северо-Восточный | Круглосуточно | 3 | 159 |
| 2 | Западный | Не круглосуточно | 141 | 150 |
| 3 | Западный | Круглосуточно | 9 | 150 |
| 17 | Южный | Круглосуточно | 1 | 131 |
| 16 | Южный | Не круглосуточно | 130 | 131 |
| 0 | Восточный | Не круглосуточно | 100 | 105 |
| 1 | Восточный | Круглосуточно | 5 | 105 |
| 14 | Юго-Западный | Не круглосуточно | 89 | 96 |
| 15 | Юго-Западный | Круглосуточно | 7 | 96 |
| 12 | Юго-Восточный | Не круглосуточно | 88 | 89 |
| 13 | Юго-Восточный | Круглосуточно | 1 | 89 |
| 8 | Северо-Западный | Не круглосуточно | 60 | 62 |
| 9 | Северо-Западный | Круглосуточно | 2 | 62 |
Промежуточный вывод
- круглосуточные кофейни в Москве — это редкость. В основном они сосредоточены в крупных районах, например, в Центральном;
- Центральный район лидирует по количеству кофеен, как круглосуточных, так и обычных;
- в районах с меньшим количеством заведений круглосуточные кофейни составляют маленькую долю. Это может указывать на то, что круглосуточные заведения чаще встречаются в зонах с высокой плотностью людей и активностью, где есть спрос на такие места.
Рейтинг кофеен по районам¶
Рассчитаем средний рейтинг кофеен по районам:
#группируем данные по округам и вычисляем средний рейтинг
mean_rating_by_district = coffee_shops.groupby(
'district')['rating'].mean().reset_index()
mean_rating_by_district['rating'] = mean_rating_by_district['rating'].round(1)
mean_rating_by_district
| district | rating | |
|---|---|---|
| 0 | Восточный административный округ | 4.3 |
| 1 | Западный административный округ | 4.2 |
| 2 | Северный административный округ | 4.3 |
| 3 | Северо-Восточный административный округ | 4.2 |
| 4 | Северо-Западный административный округ | 4.3 |
| 5 | Центральный административный округ | 4.3 |
| 6 | Юго-Восточный административный округ | 4.2 |
| 7 | Юго-Западный административный округ | 4.3 |
| 8 | Южный административный округ | 4.2 |
Отобразим средний рейтинг кофеен по районам на карте:
#определяем координаты центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
#создаем карту Москвы
m = folium.Map(location=[moscow_lat, moscow_lng],
zoom_start=10,
tiles='Cartodb Positron')
#создаем хороплет
Choropleth(
geo_data=geo_json,
data=mean_rating_by_district,
columns=['district', 'rating'],
key_on='feature.name',
fill_color='Blues',
fill_opacity=0.8,
line_opacity=0.2,
legend_name='Средний рейтинг заведений по районам',
).add_to(m)
#добавляем тултипы с названиями округов и обводки
GeoJson(geo_json,
name='districts',
style_function=lambda feature: {
'color': 'grey',
'weight': 0.8,
'fillOpacity': 0
},
tooltip=GeoJsonTooltip(fields=['name'], aliases=['Округ:'])).add_to(m)
#сохраним карту в HTML файл и отображаем в блокноте с помощью IFrame
map_path = 'moscow_map.html'
m.save(map_path)
IFrame(map_path, width=1000, height=800)
Промежуточный вывод
- в целом, рейтинги достаточно высокие;
- Центральный, Восточный, Северный, Северо-Западный и Юго-Западный округа имеют рейтинг выше (4.3), чем Западный, Северо-Восточный, Южный и Юго-Восточный округа (4.2).
Средняя стоимость чашки кофе¶
Оценим разброс данным, прежде, чем принять решение об анализе:
data['middle_coffee_cup'].describe()
count 535.000000 mean 174.721495 std 88.951103 min 60.000000 25% 124.500000 50% 169.000000 75% 225.000000 max 1568.000000 Name: middle_coffee_cup, dtype: float64
Есть явный выброс - 1568. Можем ограничить выборку от выбросов, уберем всё, что больше 500 и построим столбчатую диаграмму:
#фильтруем данные
data_cleaned = data[data['middle_coffee_cup'] <= 500]
#сокращаем названия округов
data_cleaned.loc[:, 'district'] = data_cleaned['district'].str.replace(
' административный округ', '', regex=False)
#группируем по округам и считаем среднюю цену чашки капучино
grouped_data = data_cleaned.groupby(
'district',
as_index=False).agg(avg_coffee_price=('middle_coffee_cup', 'mean'))
#округляем до целых чисел
grouped_data['avg_coffee_price'] = grouped_data['avg_coffee_price'].round(0)
#сортируем столбцы
grouped_data = grouped_data.sort_values(by='avg_coffee_price', ascending=False)
#строим диаграмму
color_palette = [
'#789DBC', '#ABBA7C', '#FFDDAE', '#C08497', '#6B4226', '#8E7DBE',
'#F4A259', '#4D8B8B', '#D1B2FF'
]
fig_bar_cleaned = px.bar(grouped_data,
x='district',
y='avg_coffee_price',
title='Средняя цена чашки капучино по районам',
labels={
'avg_coffee_price': 'Средняя цена чашки капучино',
'district': 'Округ Москвы'
},
color='district',
color_discrete_map=dict(
zip(grouped_data['district'], color_palette)))
fig_bar_cleaned.show()
Взглянем на данные в табличном формате:
grouped_data
| district | avg_coffee_price | |
|---|---|---|
| 1 | Западный | 190.0 |
| 5 | Центральный | 188.0 |
| 7 | Юго-Западный | 183.0 |
| 2 | Северный | 165.0 |
| 3 | Северо-Восточный | 165.0 |
| 4 | Северо-Западный | 160.0 |
| 8 | Южный | 158.0 |
| 6 | Юго-Восточный | 151.0 |
| 0 | Восточный | 140.0 |
Чтобы оценить размах по ценам, еще взглянем на "ящики" с ценами:
#сокращаем названия округов
data_cleaned.loc[:, 'district'] = data_cleaned['district'].str.replace(
' административный округ', '', regex=False)
#строим график
fig_box_cleaned = px.box(data_cleaned,
x='district',
y='middle_coffee_cup',
title='Цены на чашку капучино по районам',
labels={
'middle_coffee_cup': 'Цена чашки капучино',
'district': 'Округ Москвы'
})
fig_box_cleaned.update_traces(marker=dict(color='#789DBC'),
line=dict(color='#789DBC'))
fig_box_cleaned.update_layout(xaxis_title='Округ Москвы',
yaxis_title='Цена чашки капучино')
fig_box_cleaned.show()
Промежуточный вывод
- в среднем, цена на чашку капучино — около 170-180 рублей, но в реальности можно встретить как дешевые варианты (60-120 рублей), так и более дорогие (более 350 рублей);
- в Центральном и Западном районах цены выше (188–190 рублей), что может объясняться высоким спросом и более дорогой арендой;
- в более удалённых районах (Юго-Восточный, Восточный, Южный) цены ниже (140–165 рублей). Возможные причины: аренда дешевле, а покупательская способность ниже;
- для открытия кофейни стоит ориентироваться на ценовой диапазон 120-225 рублей:
- если кофейня в центре или в деловом районе, лучше ориентироваться на цену около 190 рублей;
- в периферийных районах можно установить более низкую цену, около 150-160 рублей, чтобы привлечь больше клиентов.
Корреляция ключевых параметров¶
#объединяем исходный датафрейм с новыми столбцами
coffee_shops_encoded = pd.concat([coffee_shops,
], axis=1)
#собираем список параметров для графика
cols = [
'middle_avg_bill', 'rating', 'is_24_7', 'chain', 'seats',
'middle_coffee_cup']
#рассчитываем матрицу
correlation_matrix = coffee_shops_encoded[cols].corr()
#настраиваем и строим карту
plt.figure(figsize=(12, 8))
sns.heatmap(correlation_matrix,
annot=True,
cmap='mako',
center=0,
fmt='.2f',
linewidths=0.5,
vmin=-1,
vmax=1)
plt.title('Корреляция параметров кофеин Москвы')
plt.show()
Исходя из результатов данной тепловой карты, можно отметить:
- глобально, однозначно стат значимых корреляций не выявлено;
- есть небольшая связь между средним чеком в заведении и его нахождением в ЦАО, а средним чеком и круглосуточной работой.
Вывод и рекомендации¶
В ходе анализа выявлено:¶
Центральный округ — лучший выбор:
Высокий рейтинг (4.3) и цена чашки капучино (188 руб.) говорят о платежеспособных клиентах.
Но высокая конкуренция (428 кофеен) — это большой вызов.
Северный и Северо-Восточный округа:
Отличный рейтинг (4.3), более низкая цена (165 руб.), но и меньше кофеен.
Меньше конкуренции, но и менее насыщенный рынок.
Западный округ:
Высокая цена (190 руб.), рейтинг 4.2.
Это престижный район с высокой конкуренцией, но потенциально большими доходами.
Доступные районы:
- Восточный (140 руб.)
- Юго-Восточный (151 руб.)
- Южный (158 руб.)
Низкие цены, но и менее платежеспособные клиенты. В этих районах меньше конкуренции, но нужно тщательно подходить к маркетингу.
Круглосуточные кофейни:
В центре и престижных районах кругосуточные кофейни пользуются спросом, но аренда и конкуренция могут быть дорогими.
Рекомендации:¶
Для открытия кофейни с похожим форматом на «Central Perk» необходимо учитывать параметры:
- Маркетинговое позиционирование: кофейня с уникальной концепцией для привлечения внимания в условиях высокой конкуренции;
- Расположение: ЦАО — как наиболее подходящий район;
- Средний чек: от 500–600 рублей за чашку капучино;
- График работы: в предлагаемом районе есть спрос на круглосуточные заведения. Необходимо оценить финансовую целесообразность.